package com.decent.common.util.file;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.WriteTable;
import com.decent.common.enums.ErrorCodeEnum;
import com.decent.common.exception.ErrorCodeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Excel工具类
 *
 * @author 韩涛
 * @date 2020年01月18日 22时38分
 */
@Slf4j
@SuppressWarnings("unused")
public class ExcelUtil {

    /**
     * 时间格式
     */
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

    /**
     * 同步读取Excel
     *
     * @param inputStream 输入流
     * @param head        导入类的配置信息
     * @param <T>         泛型T
     * @return 同步读取的信息
     */
    public static <T> List<T> syncReadExcel(InputStream inputStream, Class<T> head) {
        return EasyExcel.read(inputStream).head(head).doReadAllSync();
    }

    /**
     * 异步读取Excel
     *
     * @param inputStream  输入流
     * @param head         导入类的配置信息
     * @param readListener 读取监听器
     */
    public static <T> void asyncReadExcel(InputStream inputStream, Class<T> head, ReadListener<T> readListener) {
        EasyExcel.read(inputStream, head, readListener).doReadAll();
    }

    /**
     * 异步读取Excel
     *
     * @param inputStream  输入流
     * @param head         导入类的配置信息
     * @param bufferSize   缓冲区大小
     * @param saveDataFunc 保存数据函数
     */
    public static <T> void asyncReadExcel(InputStream inputStream, Class<T> head, int bufferSize,
                                          Consumer<List<T>> saveDataFunc) {
        EasyExcel.read(inputStream, head, new DefaultAnalysisEventListener<>(bufferSize, saveDataFunc)).doReadAll();
    }

    /**
     * 导出Excel
     * <p>
     * 小数据量专用,导出格式为xlsx
     *
     * @param filename  文件名
     * @param titleList 标题列
     * @param data      导出数据
     */
    public static File exportExcelFile(String filename, List<String> titleList, List<List<String>> data) {
        File excel = new File(filename + ".xlsx");
        if (excel.exists()) {
            FileUtil.del(excel);
        }
        writeDataToExcelFile(excel, null, writer ->
                writer.write(data, EasyExcel.writerSheet(filename).build(), getTable(titleList)));
        return excel;
    }

    /**
     * 导出Excel
     * <p>
     * 小数据量专用,导出格式为xlsx
     *
     * @param filename  文件名
     * @param titleList 标题列
     * @param data      导出数据
     */
    public static void exportExcel(String filename, List<String> titleList, List<List<Object>> data) {
        String filenameWithDate = filename + "_" + LocalDate.now().format(DATE_TIME_FORMATTER);
        HttpServletResponse response = getResponse(getFilenameWithUrlEncoder(filenameWithDate), "xlsx");
        writeDataToExcel(response, null, writer ->
                writer.write(data, EasyExcel.writerSheet(filenameWithDate).build(), getTable(titleList)));
    }

    /**
     * 导出Excel
     * <p>
     * 小数据量专用,导出格式为xlsx
     *
     * @param filename 文件名
     * @param head     导出类的配置信息
     * @param data     导出数据
     */
    public static <T> void exportExcel(String filename, Class<T> head, List<T> data) {
        String filenameWithDate = filename + "_" + LocalDate.now().format(DATE_TIME_FORMATTER);
        HttpServletResponse response = getResponse(getFilenameWithUrlEncoder(filenameWithDate), "xlsx");
        writeDataToExcel(response, head, writer ->
                writer.write(data, EasyExcel.writerSheet(filenameWithDate).build()));
    }


    /**
     * 导出Excel
     * <p>
     * 大数据量专用,导出格式为zip
     *
     * @param bufferSize        缓冲区大小(设值不要太大，防止OOM)
     * @param filename          文件名
     * @param titleList         标题列
     * @param pageQueryFunction 分页查询函数(分页查询,防止OOM)
     */
    public static void exportExcel(int bufferSize, String filename, List<String> titleList,
                                   BiFunction<Integer, Integer, List<List<Object>>> pageQueryFunction) {
        String filenameWithDate = filename + "_" + LocalDate.now().format(DATE_TIME_FORMATTER);
        HttpServletResponse response = getResponse(getFilenameWithUrlEncoder(filenameWithDate), "zip");
        WriteSheet writeSheet = EasyExcel.writerSheet(filenameWithDate).build();
        WriteTable writeTable = getTable(titleList);
        writeDataToZip(response, filenameWithDate, null, writer -> {
            int page = 1;
            List<List<Object>> data;
            do {
                data = pageQueryFunction.apply(page++, bufferSize);
                if (CollectionUtil.isEmpty(data)) {
                    break;
                }
                // 写数据
                writer.write(data, writeSheet, writeTable);
            } while (true);
        });
    }

    /**
     * 导出Excel
     * <p>
     * 大数据量专用,导出格式为zip
     *
     * @param bufferSize        缓冲区大小(设值不要太大，防止OOM)
     * @param filename          文件名
     * @param head              导出类的配置信息
     * @param pageQueryFunction 分页查询函数(分页查询,防止OOM)
     */
    public static <T> void exportExcel(int bufferSize, String filename, Class<T> head,
                                       BiFunction<Integer, Integer, List<T>> pageQueryFunction) {
        String filenameWithDate = filename + "_" + LocalDate.now().format(DATE_TIME_FORMATTER);
        HttpServletResponse response = getResponse(getFilenameWithUrlEncoder(filenameWithDate), "zip");
        WriteSheet writeSheet = EasyExcel.writerSheet(filenameWithDate).build();
        writeDataToZip(response, filenameWithDate, head, writer -> {
            int page = 1;
            List<T> data;
            do {
                data = pageQueryFunction.apply(page++, bufferSize);
                if (CollectionUtil.isEmpty(data)) {
                    break;
                }
                // 写数据
                writer.write(data, writeSheet);
            } while (true);
        });
    }

    /**
     * 获取ULR编码后的文件名
     *
     * @param filename 文件名
     * @return ULR编码后的文件名
     */
    private static String getFilenameWithUrlEncoder(String filename) {
        try {
            return URLEncoder.encode(filename, "utf-8");
        } catch (UnsupportedEncodingException e) {
            return LocalDate.now().format(DATE_TIME_FORMATTER);
        }
    }

    /**
     * 获取当前响应流
     *
     * @param filenameWithUrlEncoder 经过URL编码后的文件名,防止中文乱码
     * @param suffix                 文件后缀,区分文件类型
     * @return HttpServletResponse
     */
    private static HttpServletResponse getResponse(String filenameWithUrlEncoder, String suffix) {
        HttpServletResponse response = Optional.ofNullable((ServletRequestAttributes)
                        RequestContextHolder.getRequestAttributes())
                .map(ServletRequestAttributes::getResponse)
                .orElseThrow(() -> new ErrorCodeException(ErrorCodeEnum.NO, "获取响应流失败"));
        response.setContentType("multipart/form-data");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-disposition",
                String.format("attachment;filename=%s.%s", filenameWithUrlEncoder, suffix));
        return response;
    }

    /**
     * 获取ExcelWriter
     *
     * @param outputStream 输出流
     * @return ExcelWriter
     */
    private static ExcelWriter getExcelWriter(OutputStream outputStream) {
        return EasyExcel.write(outputStream)
                .excelType(ExcelTypeEnum.XLSX)
                .needHead(true).build();
    }

    /**
     * 获取表格
     *
     * @param headList 标题集合
     * @return 表格
     */
    private static WriteTable getTable(List<String> headList) {
        return EasyExcel.writerTable(0)
                .needHead(Boolean.TRUE)
                .head(getTableHead(headList))
                .build();
    }

    /**
     * 获取表头
     *
     * @param titleList 标题集合
     * @return 表头数据
     */
    private static List<List<String>> getTableHead(List<String> titleList) {
        return titleList.stream().map(Arrays::asList).collect(Collectors.toList());
    }

    /**
     * 将数据写入到Excel中
     *
     * @param response  响应信息
     * @param head      导出类的配置信息
     * @param writeFunc 写入函数
     */
    private static <T> void writeDataToExcel(HttpServletResponse response, Class<T> head,
                                             Consumer<ExcelWriter> writeFunc) {
        try (ServletOutputStream servletOutputStream = response.getOutputStream()) {
            ExcelWriter writer = Objects.nonNull(head) ? EasyExcel.write(servletOutputStream, head).build() :
                    getExcelWriter(servletOutputStream);
            // 写数据
            writeFunc.accept(writer);
            writer.finish();
        } catch (Exception e) {
            log.error("导出Excel(xlsx格式)异常", e);
            throw new ErrorCodeException(ErrorCodeEnum.NO, "导出Excel异常");
        }
    }

    /**
     * 将数据写入到Excel中
     *
     * @param excel     响应信息
     * @param head      导出类的配置信息
     * @param writeFunc 写入函数
     */
    private static <T> void writeDataToExcelFile(File excel, Class<T> head,
                                                 Consumer<ExcelWriter> writeFunc) {
        try (OutputStream out = new FileOutputStream(excel)) {
            ExcelWriter writer = Objects.nonNull(head) ? EasyExcel.write(out, head).build() :
                    getExcelWriter(out);
            // 写数据
            writeFunc.accept(writer);
            writer.finish();
        } catch (Exception e) {
            log.error("导出Excel(xlsx格式)异常", e);
            throw new ErrorCodeException(ErrorCodeEnum.NO, "导出Excel异常");
        }
    }

    /**
     * 将数据写入到ZIP压缩文件中
     *
     * @param response  响应信息
     * @param filename  文件名
     * @param head      导出类的配置信息
     * @param writeFunc 写入函数
     */
    private static <T> void writeDataToZip(HttpServletResponse response, String filename, Class<T> head,
                                           Consumer<ExcelWriter> writeFunc) {
        try (ServletOutputStream servletOutputStream = response.getOutputStream();
             ZipOutputStream zipOutputStream = new ZipOutputStream(servletOutputStream)) {
            ZipEntry zipEntry = new ZipEntry(String.format("%s.xlsx", filename));
            zipOutputStream.putNextEntry(zipEntry);
            ExcelWriter writer = Objects.nonNull(head) ? EasyExcel.write(zipOutputStream, head).build() :
                    getExcelWriter(zipOutputStream);
            writeFunc.accept(writer);
            writer.finish();
        } catch (Exception e) {
            log.error("导出Excel(zip格式)异常", e);
            throw new ErrorCodeException(ErrorCodeEnum.NO, "导出Excel异常");
        }
    }

    /**
     * 默认导入事件监听器
     *
     * @author 韩涛
     * @date 2020/10/8 17:24
     */
    private static class DefaultAnalysisEventListener<T> extends AnalysisEventListener<T> {

        /**
         * 缓冲区,防止OOM
         */
        private final List<T> buffer;
        /**
         * 缓冲区大小
         */
        private final int bufferSize;
        /**
         * 保存数据函数
         */
        private final Consumer<List<T>> saveDataFunc;

        public DefaultAnalysisEventListener(int bufferSize, Consumer<List<T>> saveDataFunc) {
            this.buffer = new ArrayList<>(bufferSize);
            this.bufferSize = bufferSize;
            this.saveDataFunc = saveDataFunc;
        }

        /**
         * 每解析一条数据都会调用此方法
         *
         * @param data    解析的数据
         * @param context excel读取器的主要描述点
         */
        @Override
        public void invoke(T data, AnalysisContext context) {
            buffer.add(data);
            if (buffer.size() < bufferSize) {
                return;
            }
            saveDataFunc.accept(buffer);
            buffer.clear();
        }

        /**
         * 所有数据解析完成时调用此方法
         *
         * @param context excel读取器的主要描述点
         */
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            if (buffer.isEmpty()) {
                return;
            }
            // 防止读取数据遗漏
            saveDataFunc.accept(buffer);
        }

    }

}